#encoding:gbk
'''
本策略事先设定好交易的股票篮子，然后根据指数的CCI指标来判断超买和超卖
当有超买和超卖发生时，交易事先设定好的股票篮子
'''
import pandas as pd
import numpy as np
import talib
from datetime import datetime, timedelta
import time
import csv

#account = "87999999"
account = "9990070005"
#accountType = 'STOCK'
accountType = 'CREDIT'
POSITION_LIMIT = 10000
ORDER_LIMIT = 400

class a():pass
A = a()
A.bought_list = []
A.waiting_dict =  {}
A.all_order_ref_dict = {}
A.trade_times = {}
#撤单间隔 单位秒 超过间隔未成交的委托撤回重报
A.withdraw_secs = 30
#定义策略开始结束时间 在两者间时进行下单判断 其他时间跳过
A.start_time = '090000'
A.end_time = '180000'
target_file = 'D:\\quantfund\\cats\\strategyframework\\target1.csv'
trade_file = 'D:\\quantfund\\cats\\strategyframework\\trade.csv'
#account = "0069932979"

def cal_time_diff1(stock_time):
    now = datetime.now().timestamp()
    print("now:", now)
    print("stock time:", stock_time)
    latency = now - stock_time
    print("time diff: ", latency)
    return latency

def cal_time_diff(time_str):
    now = datetime.now().timestamp()
    current_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
    print("current time:{},{}".format(current_time,now))
    
    hour = int(time_str[0:2])
    minute = int(time_str[2:4])
    second = int(time_str[4:6])
    millisecond = 0#int(time_str[6:])
    
    # 创建一个timedelta对象，用于表示毫秒
    milliseconds = timedelta(milliseconds=millisecond)
 
# 获取当前日期
    current_date = datetime.now().date()
 
# 创建一个datetime对象，包含当前的日期和解析出来的时间
    datetime_obj = datetime.combine(current_date, datetime.min.time()) + timedelta(hours=hour, minutes=minute, seconds=second) + milliseconds
 
    stock_timestamp = datetime_obj.timestamp()
    print("stock time:{}".format(stock_timestamp))
    
    stock_time = now - stock_timestamp
    print("time diff: {}".format(stock_time))
    return stock_time

def show_data(data):
#'m_strInsertDate': '20240222', 'm_strInsertTime': '091259'
    tdata = {}
    for ar in dir(data):
        if ar[:2] != 'm_':continue
        try:
            tdata[ar] = data.__getattribute__(ar)
        except:
            tdata[ar] = '<CanNotConvert>'
    return tdata

def read_csv_to_dict_list(filepath):
    """
    读取CSV文件，返回一个字典列表。
    每个字典代表一行数据，键是CSV的列名。
    """
    data = []
    with open(filepath, mode='r', encoding='utf-8') as csvfile:
        # 使用DictReader读取CSV，它会自动处理表头作为字典的键
        reader = csv.DictReader(csvfile)
        for row in reader:
            data.append(row) # 将字典行添加到列表中
    return data

def call_back(data):
	print(data)

def init(C):
	#下单函数的参数需要 ContextInfo对象 在init中定义行情回调函数 可以用到init函数的入参 不用手动传入 
	def callback_func(data):
# amount  close   high    low   open  openInterest  preClose  stime           settelementPrice     suspendFlag           time  volume  
#242382305.0  11.41  11.47  11.39  11.44            13     11.45   20250922      0.0  		0  	1758470400000  212240
		for stock in data:
			#print(data[stock])
			current_price = data[stock]['close'][0]
			#print(current_price)
			pre_price = data[stock]['preClose'][0]
			#print(pre_price)
			ratio = current_price / pre_price - 1
			#print('ratio:', ratio)
			#print('stime:', data[stock]['stime'][0])
			#print('time:',data[stock]['time'])
			#print(stock, C.get_stock_name(stock), '当前涨幅', ratio)
			#cal_time_diff1(data[stock]['time'][0]/1000)
			if ratio > 0 and stock not in A.bought_list:
				msg = f"当前涨幅 {ratio} 大于0 买入100股"
				print(msg)
				#下单函数passorder 安全起见处于注释状态 需要实际测试下单交易时再放开
				#passorder(23, 1101, account, stock, 5, -1, 100, '订阅下单示例', 2, msg, C)
				A.bought_list.append(stock)
	
		'''读取目标仓位 字典格式 品种代码:持仓股数, 可以读本地文件/数据库'''
	target_stocks = read_csv_to_dict_list(target_file)
	A.final_dict = {i['stock_code'] : int(i['number']) for i in target_stocks}
	#A.final_dict = {"600000.SH" :100, '000001.SZ' : 200}
	'''设置交易账号 acount accountType是界面上选的账号 账号类型'''
	A.acct = account
	A.acct_type = accountType
	#定时器 定时触发指定函数
	#C.run_time("f","5nSecond","2025-09-14 13:20:00")
	#C.run_time("return_money","1nDay","2025-09-14 14:50:00")
	#stock_list = ['600000.SH', '000001.SZ']
		#for stock in stock_list:
	#	C.subscribe_quote(stock, period = '1d', callback = callback_func)
	#stock_list = C.get_stock_list_in_sector("沪深A股")
	#sub_num_dict = {i:C.subscribe_quote(
	#	stock_code = i, 
	#	period = '1m',  
	#	dividend_type = 'none',
	#	result_type = 'dict',  # 回调函数的行情数据格式
	#	callback = call_back  # 指定一个自定义的函数接收行情，自定义的函数只能有一个位置参数
	#	) for i in stock_list}
	
	#C.stock_list = ["SZ","SH"]
	#C.subID = C.subscribe_whole_quote(C.stock_list,callback=call_back)
	C.set_account(account)


def credit_account_callback(ContextInfo,seq,result):
	print(seq)
	print(f":维持担保比例:{result.m_dPerAssurescaleValue:.2f},总负债:{result.m_dTotalDebt:.2f}")
	
def return_money(C):
	#query_credit_account(C.accid,int(time.time()),C)# 该函数必须配合credit_account_callback回调才能使用
	#获取账号信息
	acct = get_trade_detail_data(A.acct, A.acct_type, 'account')
	if len(acct) == 0:
		print(A.acct, '账号未登录 停止委托')
		return
	acct = acct[0]
	#获取可用资金
	available_cash = acct.m_dAvailable
	debt = acct.m_dTotalDebit
	print('可用资金', available_cash, '负债', debt)
	
	if available_cash > debt :
		money = debt  #还款金额
	else:
		money = available_cash
	
	#account='123456'
	s = '000001.SZ'  # 代码填任意股票，占位用
	passorder(32, 1101, account, s, 5, 0, money, 2, C)
	# passorder(75, 1101, account, s, 5, 0, money, 2, ContextInfo) 专项直接还款
	
def after_init(ContextInfo):
    # 在策略交易界面运行时，account的值会被赋值为策略配置中的账号，编辑器界面运行时，需要手动赋值
    # 编译器界面里执行的下单函数不会产生实际委托  
    #passorder(23, 1101, account, "600000.SH", 5, 0, 100, "示例", 2, "投资备注",ContextInfo)
    pass

def order_callback(ContextInfo, orderInfo):
#{'m_bEnable': True, 'm_dCancelAmount': 0.0, 'm_dFrozenCommission': 0.0, 'm_dFrozenMargin': 0.0, 'm_dLimitPrice': 11.41, 'm_dOrderPriceRMB': 0.0, 'm_dReferenceRate': 0.0, 'm_dShortOccupedMargin': 1.7976931348623157e+308, 'm_dTradeAmount': 0.0, 'm_dTradeAmountRMB': 0.0, 'm_dTradedPrice': 0.0, 'm_eCashgroupProp': 48, 'm_eCoveredFlag': 0, 'm_eEntrustType': 48, 'm_nDirection': 48, 'm_nErrorID': 2147483647, 'm_nFrontID': -1, 'm_nGroupId': 2147483647, 'm_nHedgeFlag': 49, 'm_nOffsetFlag': 48, 'm_nOpType': 23, 'm_nOrderPriceType': 50, 'm_nOrderStatus': 50, 'm_nOrderStrategyType': 3145784, 'm_nOrderSubmitStatus': 51, 'm_nRef': 1781530632, 'm_nSessionID': -1, 'm_nStrategyID': -2704095904928980719, 'm_nTaskId': 8, 'm_nVolumeTotal': 0, 'm_nVolumeTotalOriginal': 100, 'm_nVolumeTraded': 0, 'm_strAccountID': '87999999', 'm_strAccountKey': '2____11237____112372____49____87999999____', 'm_strAccountName': '', 'm_strAccountRemark': '', 'm_strBrokerName': '', 'm_strCancelInfo': '', 'm_strCompactNo': '', 'm_strErrorMsg': '', 'm_strExchangeID': 'SZ', 'm_strExchangeName': '深交所', 'm_strInsertDate': '20250922', 'm_strInsertTime': '102701', 'm_strInstrumentID': '000001', 'm_strInstrumentName': '平安银行', 'm_strLocalInfo': '', 'm_strOptName': '限价买入', 'm_strOption': '', 'm_strOrderParam': '', 'm_strOrderRef': '3284593266348039610', 'm_strOrderStrategyType': '函数下单', 'm_strOrderSysID': '', 'm_strProductID': '', 'm_strProductName': '', 'm_strRemark': '投资备注', 'm_strSource': '新建策略文件', 'm_strUnderCode': '', 'm_strXTTrade': '本终端', 'm_xtTag': '<CanNotConvert>'}
    #print("order callback")
    #print(show_data(orderInfo))
    #cal_time_diff(orderInfo.m_strInsertTime)
	pass

def deal_callback(ContextInfo, dealInfo):
#{'m_dCloseProfit': 0.0, 'm_dCommission': 2.05705, 'm_dComssion': 2.05705, 'm_dOrderPriceRMB': 0.0, 'm_dPrice': 11.41, 'm_dPriceRMB': 0.0, 'm_dReferenceRate': 0.0, 'm_dTradeAmount': 1141.0, 'm_dTradeAmountRMB': 0.0, 'm_eCoveredFlag': 48, 'm_eEntrustType': 48, 'm_eFutureTradeType': 48, 'm_nCloseTodayVolume': 0, 'm_nDirection': 48, 'm_nGroupId': 2147483647, 'm_nHedgeFlag': 49, 'm_nOffsetFlag': 48, 'm_nOrderPriceType': 50, 'm_nOrderStrategyType': 0, 'm_nRealOffsetFlag': -1, 'm_nRef': 1781530632, 'm_nStrategyID': -2704095904928980719, 'm_nTaskId': 8, 'm_nVolume': 100, 'm_strAccountID': '87999999', 'm_strAccountKey': '2____11237____112372____49____87999999____', 'm_strAccountRemark': '', 'm_strCompactNo': '', 'm_strExchangeID': 'SZ', 'm_strExchangeName': '深交所', 'm_strInstrumentID': '000001', 'm_strInstrumentName': '平安银行', 'm_strLocalInfo': '', 'm_strOperation': '', 'm_strOptName': '限价买入', 'm_strOrderRef': '3284593266348039610', 'm_strOrderStrategyType': '函数下单', 'm_strOrderSysID': 'E100069773', 'm_strProductID': '', 'm_strProductName': '', 'm_strRemark': '投资备注', 'm_strSource': '新建策略文件', 'm_strTradeDate': '20250922', 'm_strTradeID': '500001406_000001', 'm_strTradeTime': '102701', 'm_strXTTrade': '本终端', 'm_xtTag': '<CanNotConvert>'}
    #print("deal callback")
    #print(show_data(dealInfo))
    #cal_time_diff(dealInfo.m_strTradeTime)
	pass
	
def handlebar(ContextInfo):
    if not ContextInfo.is_last_bar():
        return
    
    # 单股单账号股票最新价买入 100 股（1 手），快速下单参数设置为 1   
    #passorder(23, 1101, account, "000001.SZ", 5, 0, 100, "示例", 1, "投资备注",ContextInfo)
    
    # 单股单账号股票指定价买入 100 股（1 手），快速下单参数设置为 1
#    passorder(23, 1101, account, "000001.SZ", 11, 7, 100, "示例", 1, "投资备注",ContextInfo)
def get_open_order(code):
	order_list = get_trade_detail_data(A.acct, 'stock', 'order')
	for order in order_list:
		if (order.m_strInstrumentID+ '.' + order.m_strExchangeID == code):
			print (f"{order.m_strInstrumentID}.{order.m_strExchangeID}:status:{order.m_nOrderStatus}")
			if order.m_nOrderStatus in [48,49,50,51,52,55,86,255]:
				return order
	return None
	
def calc_hedge_amount(abspos):
    if abspos >= 100:
        return 100
    elif abspos >= 50:
        return 50
    else:
        return abspos

def f(C):
	'''定义定时触发的函数 入参是ContextInfo对象'''
	#记录本次调用时间戳
	t0 = time.time()
	final_dict=A.final_dict
	#本次运行时间字符串
	now = datetime.now()
	now_timestr = now.strftime("%H%M%S")
	#跳过非交易时间
	if now_timestr < A.start_time or now_timestr > A.end_time:
		return
	#获取账号信息
	acct = get_trade_detail_data(A.acct, A.acct_type, 'account')
	if len(acct) == 0:
		print(A.acct, '账号未登录 停止委托')
		return
	acct = acct[0]
	#获取可用资金
	available_cash = acct.m_dAvailable
	print(now, '可用资金', available_cash)
	#获取持仓信息
	position_list = get_trade_detail_data(A.acct, A.acct_type, 'position')
	#持仓数据 组合为字典
	position_dict = {i.m_strInstrumentID + '.' + i.m_strExchangeID : int(i.m_nVolume) for i in position_list}
	position_dict_available = {i.m_strInstrumentID + '.' + i.m_strExchangeID : int(i.m_nCanUseVolume) for i in position_list}
	#未持有的品种填充持股数0
	not_in_position_stock_dict = {i : 0 for i in final_dict if i not in position_dict}
	position_dict.update(not_in_position_stock_dict)
	#print(position_dict)
	stock_list = list(position_dict.keys())
	# print(stock_list)
	#获取全推行情
	full_tick = C.get_full_tick(stock_list)
	#print('fulltick', full_tick)
	#更新持仓状态记录
	refresh_waiting_dict(C)
	#撤超时委托
	order_list = get_trade_detail_data(A.acct, 'stock', 'order')
	if '091500'<= now_timestr <= '093000':#指定的范围內不撤单
		pass
	else:
		for order in order_list:
			#非本策略 本次运行记录的委托 不撤
			if order.m_strRemark not in A.all_order_ref_dict:
				continue
			#委托后 时间不到撤单等待时间的 不撤
			if time.time() - A.all_order_ref_dict[order.m_strRemark] < A.withdraw_secs:
				continue
			#对所有可撤状态的委托 撤单
			if order.m_nOrderStatus in [48,49,50,51,52,55,86,255]:
				print(f"超时撤单 停止等待 {order.m_strRemark}")
				cancel(order.m_strOrderSysID,A.acct,'stock',C)
	th = 2  # 2bps
	#下单判断
	for stock in position_dict:
		if stock not in A.trade_times:
			A.trade_times[stock] = 0
		#有未查到的委托的品种 跳过下单 防止超单
		#if stock in A.waiting_dict:
		#	print(f"{stock} 未查到或存在未撤回委托 {A.waiting_dict[stock]} 暂停后续报单")
		#	continue
		if stock in position_dict.keys():
			#print(position_dict[stock],target_vol,'1111')
			#到达目标数量的品种 停止委托
			target_vol = final_dict[stock] if stock in final_dict else 0
			if int(abs(position_dict[stock] - target_vol)) == 0:
				print(stock, C.get_stock_name(stock), '与目标一致')
				continue
			#与目标数量差值小于100股的品种 停止委托
			#if abs(position_dict[stock] - target_vol) < 100:
			#	print(f"{stock} {C.get_stock_name(stock)} 目标持仓{target_vol} 当前持仓{position_dict[stock]} 差额小于100 停止委托")
			#	continue
			currbid = full_tick[stock]['bidPrice'][0]
			currbsize = full_tick[stock]['bidVol'][0]
			currask = full_tick[stock]['askPrice'][0]
			currasize = full_tick[stock]['askVol'][0]
			if int(currasize) == 0 and int(currbsize) == 0:
				continue
			else:
				fv = (currbid * currasize + currask * currbsize) / (currbsize + currasize)
			order = get_open_order(stock)
			if order != None:
				order_edge = abs(fv - order.m_dLimitPrice)  / fv * 1e4
				if (order_edge > 20) | (order_edge < th):
					print(f"撤单 edge {order_edge} order id:{order.m_strOrderSysID} price:{order.m_dLimitPrice} {order.m_strRemark}")
					cancel(order.m_strOrderSysID,A.acct,'stock',C)
				continue
			#持仓大于目标持仓 卖出
			if position_dict[stock]>target_vol:
				price = max(currask - 0.001, round(fv * (1 + th / 1e4), 3))
				#vol = int((position_dict[stock] - target_vol)/100)*100
				vol = position_dict[stock] - target_vol
				if stock not in position_dict_available:
					continue
				vol = min(vol, position_dict_available[stock])
				vol = calc_hedge_amount(vol)
				
				#价无效时 跳过委托
				if not price > 0 or vol <= 0:
					print(f"{stock} {C.get_stock_name(stock)} 取到的价格{price}无效，或者vol<=0跳过此次推送")
					continue
				A.trade_times[stock] += 1
				if A.trade_times[stock] > ORDER_LIMIT :
					print("trade times exceed ORDER_LIMIT 400")
					continue
				print(stock,'应该卖出')
				print(f"{stock} {C.get_stock_name(stock)} 目标股数{target_vol} 当前股数{position_dict[stock]}")
				msg = f"{now.strftime('%Y%m%d%H%M%S')}_{stock}_sell_{vol}股"
				print(msg)
				#指定价卖出,担保品卖出
				passorder(34,1101,A.acct,stock,11,price,vol,'调仓策略',2,msg,C)
				
				A.waiting_dict[stock] = msg
				A.all_order_ref_dict[msg] = time.time()
			#持仓小于目标持仓 买入
			if position_dict[stock]<target_vol:
				#vol = int((target_vol-position_dict[stock])/100)*100
				vol = target_vol-position_dict[stock]
				vol = calc_hedge_amount(vol)
				if position_dict[stock] + vol >= POSITION_LIMIT :
					print("exceed POSITION_LIMIT 10000")
					continue
				#获取价
				price = min(currbid + 0.001, round(fv * (1 - th / 1e4), 3))
				#价无效时 跳过委托
				if not price > 0:
					print(f"{stock} {C.get_stock_name(stock)} 取到的价格{price}无效，跳过此次推送")
					continue
				A.trade_times[stock] += 1
				if A.trade_times[stock] > ORDER_LIMIT :
					print("trade times exceed ORDER_LIMIT 400")
					continue
				target_value = price * vol

				print(f"{stock} {C.get_stock_name(stock)} 目标股数{target_vol} 当前股数{position_dict[stock]}")
				msg = f"{now.strftime('%Y%m%d%H%M%S')}_{stock}_buy_{vol}股"
				print(msg)
				if target_value > available_cash:
					print(f"{stock} 目标市值{target_value} 大于 可用资金{available_cash} 融资买入")
					passorder(27,1101,A.acct,stock,11,price,vol,'调仓策略',2,msg,C)
				else:
				#价买入
					passorder(33,1101,A.acct,stock,11,price,vol,'调仓策略',2,msg,C)
				A.waiting_dict[stock] = msg
				A.all_order_ref_dict[msg] = time.time()
				available_cash -= target_value
	#打印函数运行耗时 定时器间隔应大于该值
	print(f"下单判断函数运行完成 耗时{time.time() - t0}秒")


def refresh_waiting_dict(C):
	"""更新委托状态 入参为ContextInfo对象"""
	#获取委托信息
	order_list = get_trade_detail_data(A.acct,A.acct_type,'order')
	#取出委托对象的 投资备注 : 委托状态
	ref_dict = {i.m_strRemark : int(i.m_nOrderStatus) for i in order_list}
	del_list = []
	for stock in A.waiting_dict:
		if A.waiting_dict[stock] in ref_dict and ref_dict[A.waiting_dict[stock]] in [56, 53, 54]:
			#查到对应投资备注 且状态为成交 / 已撤 / 部撤， 从等待字典中删除
			print(f'查到投资备注 {A.waiting_dict[stock]}，的委托 状态{ref_dict[A.waiting_dict[stock]]} (56已成 53部撤 54已撤)从等待等待字典中删除')
			del_list.append(stock)
		if A.waiting_dict[stock] in ref_dict and ref_dict[A.waiting_dict[stock]] == 57:
			#委托状态是废单的 也停止等待 从等待字典中删除 
			print(f"投资备注为{A.waiting_dict[stock]}的委托状态为废单 停止等待")
			del_list.append(stock)
	for stock in del_list:
		del A.waiting_dict[stock]
